/*
 * Copyright 2009-2010 New Atlanta Communications, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.newatlanta.appengine.datastore;

import static com.google.appengine.api.datastore.DatastoreServiceFactory.getDatastoreService;

import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import org.datanucleus.query.compiler.JDOQLParser;
import org.datanucleus.query.node.Node;
import org.datanucleus.store.query.QueryCompilerSyntaxException;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.QueryResultIterable;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.appengine.api.datastore.QueryResultList;

/**
 * Implements a JDOQL-like query language for use with the Google App Engine (Java)
 * datastore low-level API.
 * 
 * Here's the full syntax of JDOQL 2.0.
 *  
 *    http://www.docstoc.com/docs/2160042/JDOQL-20-Quick-Reference-Guide)
 *
 * Items in square brackets are optional. Query clauses must be in the specified
 * order.
 * 
 *     select [unique] [<result>] [into <result-class-name>]
 *     [from <candidate-class-name> [exclude subclasses]]
 *     [where <filter>]
 *     [variables <variable-list>]
 *     [parameters <parameter-list>]
 *     [imports <import-list>]
 *     [group by <grouping-clause>]
 *     [order by <ordering-clause>]
 *     [range <from-index>, <to-index>]
 *
 * Here's what we currently support, as implemented below, based on the
 * com.google.api.datastore.Query class:
 *
 *     select from <kind>
 *     [where <filter>]
 *     [order by <sort>]
 *     [range <from-index>, <to-index>]
 *
 * We also only support a limited number of operators in the where clause.
 *
 * Note that "range" is 1-based; if you execute the following query:
 *  
 *     select from Employee
 *     range 5, 10
 *  
 * you'll get 5 rows, the 5th through 9th.
 *  
 * This class uses the org.datanucleus.query.compiler.JDOQLParser class for
 * partial parsing of the query string. You can find the JDOQLParser source
 * code here:
 *  
 * http://datanucleus.svn.sourceforge.net/viewvc/datanucleus/platform/core/trunk/src/java/org/datanucleus/query/compiler/
 * 
 * @author <a href="mailto:vbonfanti@gmail.com">Vince Bonfanti</a>
 */
public class LowLevelQuery implements PreparedQuery {

    private JDOQLParser parser = new JDOQLParser( null );
    
    private PreparedQuery preparedQuery;
    private FetchOptions options = FetchOptions.Builder.withOffset( 0 );

    /**
     * Static methods to parse and execute a query string.
     */
    public static Iterable<Entity> asIterable( String queryString ) {
        return new LowLevelQuery( queryString ).asIterable();
    }
    
    public static Iterator<Entity> asIterator( String queryString ) {
        return new LowLevelQuery( queryString ).asIterator();
    }
    
    public static List<Entity> asList( String queryString ) {
        return new LowLevelQuery( queryString ).asList();
    }
    
    public static QueryResultIterable<Entity> asQueryResultIterable( String queryString ) {
        return new LowLevelQuery( queryString ).asQueryResultIterable();
    }
    
    public static QueryResultIterator<Entity> asQueryResultIterator( String queryString ) {
        return new LowLevelQuery( queryString ).asQueryResultIterator();
    }
    
    public static QueryResultList<Entity> asQueryResultList( String queryString ) {
        return new LowLevelQuery( queryString ).asQueryResultList();
    }
    
    public static Entity asSingleEntity( String queryString ) {
        return new LowLevelQuery( queryString ).asSingleEntity();
    }
    
    /**
     * Constructors
     */
    @SuppressWarnings("unused")
    private LowLevelQuery() {
    }
    
    public LowLevelQuery( String queryString ) {
        prepare( queryString );
    }
    
    public PreparedQuery getPreparedQuery() {
        return preparedQuery;
    }
    
    public FetchOptions getOptions() {
        return options;
    }
    
    @Override
    public Iterable<Entity> asIterable() {
        return preparedQuery.asIterable( options );
    }
    
    @Override
    public Iterable<Entity> asIterable( FetchOptions fetchOptions ) {
        return preparedQuery.asIterable( fetchOptions );
    }
    
    @Override
    public Iterator<Entity> asIterator() {
        return preparedQuery.asIterator( options );
    }
    
    @Override
    public Iterator<Entity> asIterator( FetchOptions fetchOptions ) {
        return preparedQuery.asIterator( fetchOptions );
    }
    
    public List<Entity> asList() {
        return preparedQuery.asList( options );
    }
    
    @Override
    public List<Entity> asList( FetchOptions fetchOptions ) {
        return preparedQuery.asList( fetchOptions );
    }
    
    @Override
    public QueryResultIterable<Entity> asQueryResultIterable() {
        return preparedQuery.asQueryResultIterable();
    }

    @Override
    public QueryResultIterable<Entity> asQueryResultIterable( FetchOptions fetchOptions ) {
        return preparedQuery.asQueryResultIterable( fetchOptions );
    }

    @Override
    public QueryResultIterator<Entity> asQueryResultIterator() {
        return preparedQuery.asQueryResultIterator();
    }

    @Override
    public QueryResultIterator<Entity> asQueryResultIterator( FetchOptions fetchOptions ) {
        return preparedQuery.asQueryResultIterator( fetchOptions );
    }
    
    public QueryResultList<Entity> asQueryResultList() {
        return preparedQuery.asQueryResultList( options );
    }

    @Override
    public QueryResultList<Entity> asQueryResultList( FetchOptions fetchOptions ) {
        return preparedQuery.asQueryResultList( fetchOptions );
    }
    
    @Override
    public Entity asSingleEntity() {
        return preparedQuery.asSingleEntity();
    }
    
    @Override
    public int countEntities() {
        return preparedQuery.countEntities();
    }
    
    private void prepare( String queryString ) {
        StringTokenizer st = new StringTokenizer( queryString );

        // first two tokens must be "select from"
        if ( ( st.countTokens() < 3 ) || !st.nextToken().equalsIgnoreCase( "select" )
                || !st.nextToken().equalsIgnoreCase( "from" ) )
        {
            throw new QueryCompilerSyntaxException( "expected: \"select from <kind>\"" );
        }

        // third token must be the <kind> of entity we're searching for
        Query q = new Query( st.nextToken() );

        // parse the rest of the query, clauses must be in order
        if ( st.hasMoreTokens() ) {
            String t = st.nextToken();

            if ( !t.equalsIgnoreCase( "where" ) && !t.equalsIgnoreCase( "order" ) &&
                    !t.equalsIgnoreCase( "range" ) )
            {
                throw new QueryCompilerSyntaxException( "expected: \"where\" or \"order by\" or \"range\"" );
            }

            if ( t.equalsIgnoreCase( "where" ) ) {
                StringBuilder sb = new StringBuilder();
                while ( st.hasMoreTokens() ) {
                    t = st.nextToken();
                    if ( t.equalsIgnoreCase( "order" ) || t.equalsIgnoreCase( "range" ) ) {
                        break;
                    } else {
                        sb.append( t ).append( ' ' );
                    }
                }
                addFilters( parser.parse( sb.toString() ), q );
            }

            if ( t.equalsIgnoreCase( "order" ) ) {
                if ( !st.nextToken().equalsIgnoreCase( "by" ) || !st.hasMoreTokens() ) {
                    throw new QueryCompilerSyntaxException( "expected: \"order by\"" );
                }
                StringBuilder sb = new StringBuilder();
                while ( st.hasMoreTokens() ) {
                    t = st.nextToken();
                    if ( t.equalsIgnoreCase( "range" ) ) {
                        break;
                    } else {
                        sb.append( t ).append( ' ' );
                    }
                }
                addSort( sb.toString(), q );
            }

            if ( t.equalsIgnoreCase( "range" ) ) {
                int fromIndex = Integer.parseInt( st.nextToken( "," ).trim() );
                if ( fromIndex < 1 ) {
                    throw new IllegalArgumentException( "range: <from-index> must be 1 or greater" );
                }
                int toIndex = Integer.parseInt( st.nextToken().trim() );
                if ( toIndex <= fromIndex ) {
                    throw new IllegalArgumentException( "range: <to-index> must be greater than <from-index>" );
                }
                options = options.limit( toIndex - 1 ).offset( fromIndex - 1 );
            }

            if ( st.hasMoreTokens() ) {
                throw new QueryCompilerSyntaxException( "unexpected token(s) near: " + st.nextToken() );
            }
        }
        
        preparedQuery = getDatastoreService().prepare( q );
    }

    private static void addFilters( Node n, Query q ) {
        String nodeValue = n.getNodeValue().toString();
        if ( nodeValue.equals( "&&" ) ) {
            addFilters( n.getFirstChild(), q );
            addFilters( n.getNextChild(), q );
            return;
        }

        Query.FilterOperator op = Query.FilterOperator.EQUAL;
        if ( nodeValue.equals( "==" ) ) {
            // do nothing, already initialized to correct value
        } else if ( nodeValue.equals( ">" ) ) {
            op = Query.FilterOperator.GREATER_THAN;
        } else if ( nodeValue.equals( ">=" ) ) {
            op = Query.FilterOperator.GREATER_THAN_OR_EQUAL;
        } else if ( nodeValue.equals( "<" ) ) {
            op = Query.FilterOperator.LESS_THAN;
        } else if ( nodeValue.equals( "<=" ) ) {
            op = Query.FilterOperator.LESS_THAN_OR_EQUAL;
        } else {
            throw new QueryCompilerSyntaxException( "unsupported operator: " + nodeValue );
        }

        String propertyName = n.getFirstChild().getNodeValue().toString();
        Object propertyValue = n.getNextChild().getNodeValue();
        
        q.addFilter( propertyName, op, propertyValue );
    }

    private void addSort( String s, Query q ) {
        Node[] nodeArray = parser.parseOrder( s );
        for ( Node node : nodeArray ) {
            String propertyName = node.getFirstChild().getNodeValue().toString();
            Query.SortDirection sortDirection = Query.SortDirection.ASCENDING;
            Node nextChild = node.getNextChild();
            if ( ( nextChild != null ) && nextChild.getNodeValue().toString().startsWith( "desc" ) ) {
                sortDirection = Query.SortDirection.DESCENDING;
            }
            q.addSort( propertyName, sortDirection );
        }
    }
}
